Add an llms.txt index for the documentation (#660) - #661
Conversation
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Wired into both workflows. build.yml ignores docs/**, so on its own the gate would never fire on the change that invalidates the file. build-skip.yml is the workflow that handles docs-only PRs, so it runs the target instead of echoing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Code-review follow-ups. None changes the generated file today; each closes a way it could go wrong or crash on a page the docs do not have yet. - _category_.json without a 'label' crashed instead of falling back to the slug. GetPropertyValue throws when the property is absent, so the guard and the fallback below it were dead code. - Files and directories whose name starts with an underscore are skipped. Docusaurus excludes them from routing and docs/website/_snippets already exists, so indexing one emitted a URL the site never serves. - .mdx pages are indexed too. Docusaurus routes them, and the gate could not have caught their absence: it only compares the generator to itself. - 'sidebar_position' now orders pages inside a section, as it does on the site. 07-ide has no numeric prefixes and rider.md relies on it. - Titles are read after the frontmatter and outside code fences, so a page opening with a '# terminal-command' marker cannot take it as its link text. - Unclosed frontmatter fails instead of being shipped as a description, and a YAML block scalar no longer renders as a bare '>'. - A missing introduction.md now says so instead of throwing a bare InvalidOperationException from Single(). - Titles sort with StringComparer.Ordinal. The file is verified byte for byte, so culture-sensitive ordering could fail the gate with no real drift. - build-skip.yml gets build.yml's concurrency group now that it runs a build. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
dennisdoomen
left a comment
There was a problem hiding this comment.
Automated review pass (correctness, design, and style).
| # would otherwise stack concurrent builds, which is the cost this file exists to | ||
| # avoid in the first place. | ||
| concurrency: | ||
| group: ${{ github.workflow }}-${{ github.ref }} |
There was a problem hiding this comment.
🔧🤖 Same concurrency group as build.yml (both name: build) — a PR touching code + a .md file triggers both workflows, and cancel-in-progress lets one cancel the other.
| return end + 1; | ||
| } | ||
|
|
||
| static Dictionary<string, string> ReadFrontmatter(string[] lines, int frontmatterEnd) |
There was a problem hiding this comment.
♻️🤖 This hand-rolled frontmatter parser duplicates Fallout.Utilities.Text.Yaml.YamlExtensions.GetYaml<T> (with UnderscoredNamingConvention), which already exists in this repo.
| continue; | ||
| } | ||
|
|
||
| var isProse = !insideFence && |
There was a problem hiding this comment.
🤔🤖 Excludes headings/tables/images/imports but not list items (-/*) or blockquotes (>) — a future page opening with a list would garble the description.
| static Dictionary<string, string> ReadFrontmatter(string[] lines, int frontmatterEnd) | ||
| { | ||
| var entries = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); | ||
| for (var i = 1; i < Math.Max(frontmatterEnd - 1, 1); i++) |
There was a problem hiding this comment.
⛏🤖 Math.Max(frontmatterEnd - 1, 1) is a no-op: GetFrontmatterEnd only ever returns 0 or ≥2, so the clamp to 1 never changes the loop bound.
| // Docusaurus orders pages by a numeric prefix on the directory and file name, and strips that | ||
| // prefix from the served URL. So 01-getting-started/01-installation.md is served at | ||
| // /docs/getting-started/installation. | ||
| static readonly Regex OrderPrefix = new(@"^(?<order>\d+)-", RegexOptions.Compiled); |
There was a problem hiding this comment.
🔧🤖 OrderPrefix/InlineLink/FrontmatterEntry (private static readonly, non-const) should be camelCase per .editorconfig's naming rule, like unicodeRegex elsewhere in src/.
| { | ||
| Assert.True( | ||
| GitHasCleanWorkingCopy(), | ||
| "docs/llms.txt is out of sync with docs/website. Run './build.ps1 GenerateLlmsTxt' locally and commit the result."); |
There was a problem hiding this comment.
⛏🤖 Line is 132 chars, over .editorconfig's max_line_length = 130.
| using Fallout.Common.Utilities.Collections; | ||
| using Serilog; | ||
| using static Fallout.Common.Tools.Git.GitTasks; | ||
|
|
There was a problem hiding this comment.
🔧🤖 Member order is bottom-up (helpers → entry points) and fields are split into two clusters; RewriteCsprojsStep.cs shows this repo's convention is fields-grouped, then caller-before-callee.
Implements #660.
Closes #660.
Generates
docs/llms.txt, an llmstxt.org index of the 37 pages underdocs/website/, and fails the build when it goes stale.llms.txtlets an LLM answer questions about Fallout from one fetch instead of crawling the site or falling back on stale NUKE knowledge. That fallback is the part that matters after the rebrand: a model trained before it will sayNuke.*andnuke :setup.What is here
build/Build.Documentation.cs(new)GenerateLlmsTxtandVerifyLlmsTxtdocs/llms.txt(new, generated)build/Build.CI.GitHubActions.csVerifyLlmsTxtadded to thebuildworkflow's targets.github/workflows/build.yml.github/workflows/build-skip.ymlVerifyLlmsTxtinstead of echoing.fallout/build.schema.jsonNot hand-maintained, by design. It follows the
GenerateTools/VerifyGeneratedToolspattern already in this repo: generate, commit, and fail CI when a source edit lands without regenerating.Decisions worth reviewing
The gate is wired into both workflows, and that is the point.
build.ymlhaspaths-ignore: docs/**, so a PR editing a page underdocs/website/never runs it. That is exactly the change that invalidatesllms.txt. Wiring the gate only there would have produced a check that can never fire on the thing it guards.build-skip.ymlis the workflow that handles those PRs, so it now runsVerifyLlmsTxtrather than anecho. It keeps theubuntu-latestjob name that branch protection keys on, and it builds the build project and regenerates one file rather than running test and pack.Metadata comes from what the site already declares, not from new fields. Section names are read from each directory's Docusaurus
_category_.jsonlabel, so they read "CI/CD Support" and "Common Tasks" exactly as the sidebar does. Title-casing the slug instead would have produced "Cicd" and "Ide". Titles fall back to the first H1 when a page declares notitle, which is what Docusaurus does and whatbadge.mdrelies on. Descriptions fall back to the page's opening paragraph, since onlyintroduction.mddeclares one.The base URL is
https://docs.fallout.build/docs/, verified against the livesitemap.xml. All 37 derived URLs match the 37 live pages exactly, set against set. The/docs/prefix is easy to miss: see the follow-up below.Test plan
https://docs.fallout.build/sitemap.xml. Identical in both directions, so no derived URL 404s and no live page is missing../build.ps1 GenerateLlmsTxttwice in a row leavesgit statusclean. The generator is deterministic, which is what keeps the gate from flapping../build.ps1 VerifyLlmsTxton a clean tree exits 0.title:, committed without regenerating, and the gate failed withdocs/llms.txt is out of sync with docs/website. Run './build.ps1 GenerateLlmsTxt' locally and commit the result.Reverted afterwards.title:and the target failed naming the offending file../build.ps1 VerifyGeneratedTools VerifyLlmsTxt Test, the same target set CI runs: 814 passed, 7 skipped, 0 failed, and the working tree stayed clean, so both verify gates passed../build.ps1 Compileexits 0. 8 warnings, all pre-existingNU190xadvisories, none in the new file.##sections, 37 link lines.Review round
A high-effort review raised 12 findings. Ten are fixed in
7383c59e; none changes the generated file today, and each closes a way it could go wrong on a page the docs do not have yet. The two that mattered most:_category_.jsonwithout alabelcrashed the target.GetPropertyValuethrows when the property is absent, so the guard and the slug fallback under it were dead code.docs/website/_snippets/already exists, so a.mddropped there would have emitted a URL the site never serves.Also fixed:
.mdxpages are indexed,sidebar_positionorders pages inside a section as it does on the site, titles are read outside code fences, unclosed frontmatter fails instead of shipping garbage, and titles sort withStringComparer.Ordinalso a different culture cannot fail the byte-for-byte gate with no real drift.Verified empirically rather than by reading: added
_snippets/partial.md, a.mdxpage withsidebar_position: 1, and a label-less_category_.json, regenerated, and confirmed the snippet was skipped, the MDX page indexed and sorted first, and the label-less category fell back to its slug instead of throwing. Reverted afterwards;docs/llms.txtis byte-identical.One finding dismissed. The review flagged that
docs/llms.txtsits outsidedocs/website/and might never be published. That path is the deliberate cross-repo contract, not an oversight: it is an artifact of the website sources rather than a page in them, and Fallout-build/docs.fallout.build#14 names the exact path to copy. Worth confirming with whoever owns the site build.Serving half is a separate repo
docs.fallout.buildis built fromFallout-build/docs.fallout.build. This PR generates the file; that repo has to copy it tostatic/llms.txt. Filed as Fallout-build/docs.fallout.build#14.Neither half is user-visible alone.
https://docs.fallout.build/llms.txtstays 404 until both land. That is expected, not an oversight.Plan
Follow-ups
Not fixed here, to keep this PR to its issue.
README.md:59links tohttps://docs.fallout.build/getting-started/installation, which 404s. It is missing the/docs/prefix. Found while verifying the URL derivation: the issue cited this link as proof of the mapping, and it turned out to be the stale one. It is the only such link in the repo.llms-full.txt(the corpus inlined rather than linked). Cheap now that the walk exists, but a different file with a different size budget.build-skip.ymlnow has a real build step, which makes it the natural home for other docs-only validation, such as link checking. Nothing is broken; it is just no longer true that docs PRs run nothing.